Notify listeners when days off are removed, and add the missing removal methods - #2830
Conversation
HumanResource has addDaysOff() but no counterpart: the only way to remove an absence is to mutate the DefaultListModel handed out by getDaysOff(), and nothing observes that model. Adding resets the cached LoadDistribution and fires; removing does neither. The resource properties dialog clears the list before writing the edited intervals back. With intervals left the last addDaysOff() repairs both by accident; delete the LAST absence and no event is fired at all. Measured on screen: the absence keeps being drawn on the resource chart while the dialog's list is already empty. Fixed at the root rather than at the call site -- the resource now observes its own list, so every mutation through getDaysOff() resets the loads and notifies, including callers added later. addDaysOff() drops its own resetLoads()/fireResourceChanged() so that it does not fire twice. Safe for the copying constructor: areEventsEnabled is false there before the copy loop runs, and instance initialisers run before the constructor body, so the listener is installed and stays silent.
A day off could be given to a resource through addDaysOff(), but there was no way to take one away again: the only route was to reach into the DefaultListModel handed out by getDaysOff() and mutate it. That is what the resource properties dialog did, and it left HumanResource with a collection that has an entry point but no exit. Add the two missing counterparts, following the shape HumanResourceManager already uses for its own list of resources (add / remove / clear): boolean removeDaysOff(GanttDaysOff) -- take a single interval away void clearDaysOff() -- take all of them away removeDaysOff returns whether anything was removed, because that is also the answer to "did the listeners hear about it": the list stays silent when the interval was not there. Without the return value a caller would have to look into the handed-out list again, which is the thing being avoided. It is named clearDaysOff rather than plain clear() because a resource holds assignments and custom properties too. Matching is by Object.equals, the way the list itself matches. GanttDaysOff only overloads equals(GanttDaysOff) and does not override equals(Object), so an interval built afresh from the same two dates is not the one the resource holds. That is documented on the method and pinned by a test rather than changed here. GanttDialogPerson.applyChanges() now calls clearDaysOff() instead of getDaysOff().clear(). No production code writes to the handed-out list any more. The number of notifications does not change: clearing and then writing N intervals back still costs (M > 0 ? 1 : 0) + N resourceChanged events, because clearDaysOff() calls the very same DefaultListModel.clear() the dialog used to call itself. A test walks every combination of M and N in 0..3 through both routes and pins the absolute counts, so a future implementation that fires once per interval would be caught. getDaysOff() is deliberately left exactly as it was -- signature, return type and body. Narrowing it is a separate change; the list it hands out is still mutable, so the old route remains open to anyone who takes it.
|
@Natalie-the-technician maybe it is a good time to replace |
|
Happy to. I looked at what depends on it first, and there is one consequence worth
No test touches The consequence: this PR fixes the missing notification by observing the list — That is a better end state than what is in the PR now, and it makes the three Two things I would rather have your preference on than guess:
I will fold it into this PR rather than open a second one, since without the |
|
I believe it must be an unmodifiable view with the List type. |
DefaultListModel is a Swing class: despite the name it models a UI control, not a resource. HumanResource::getDaysOff() now returns List<GanttDaysOff>, and the field behind it is a plain ArrayList with Collections.unmodifiableList over it. A view rather than a copy, so a caller holding on to it keeps seeing the resource. The notification had to move into the mutating methods. A DefaultListModel can be watched with a ListDataListener, which is how the removal of the last day off came to notify its listeners; a plain List cannot be watched. addDaysOff, removeDaysOff and clearDaysOff now reset the load distribution and fire themselves. That only holds because the getter turned tight at the same time: while the handed-out list was modifiable, a caller could change the resource without either of the two happening, which is the defect this branch fixes. The number of notifications is unchanged. removeDaysOff stays silent and returns false when nothing was removed, and clearDaysOff stays silent when the list was already empty -- exactly what DefaultListModel.removeElement() and clear() did. The resource properties dialog's clear-all-and-rewrite therefore still costs (M > 0 ? 1 : 0) + N notifications, and the test walking every M and N in 0..3 still pins those numbers. The reading callers move from getSize()/getElementAt(i) to size()/get(i); LoadDistribution and ProjectFileExporter read through a raw DefaultListModel and can drop their casts. The copying constructor fills the list directly instead of going through addDaysOff(), so copying stays silent without depending on the areEventsEnabled flag for the days off at all.
|
Done — The field behind it is a plain Two places where this was more than a type swap:
Two new tests pin the return value itself: that modifying it throws
|
dbarashev
left a comment
There was a problem hiding this comment.
Looks good. A few relatively minor comments below.
| if (p.getDaysOff() != null) | ||
| for (int j = 0; j < p.getDaysOff().size(); j++) { | ||
| GanttDaysOff gdo = (GanttDaysOff) p.getDaysOff().getElementAt(j); | ||
| GanttDaysOff gdo = p.getDaysOff().get(j); |
There was a problem hiding this comment.
Can you please save days off into a local variable, to avoid calling it (and creating a read-only view) many times?
There was a problem hiding this comment.
Any sense to join these three test classes into a single one?
| }); | ||
|
|
||
| person.getDaysOff().clear(); | ||
| person.clearDaysOff(); |
There was a problem hiding this comment.
maybe we can introduce setDaysOff() method which would replace the entire list (and send just one notification) ?
| private void processDaysOff(HumanResource resource) { | ||
| DefaultListModel daysOff = resource.getDaysOff(); | ||
| List<GanttDaysOff> daysOff = resource.getDaysOff(); | ||
| if (daysOff != null) { |
| @@ -36,7 +36,7 @@ void save(IGanttProject project, TransformerHandler handler) throws SAXException | |||
| for (HumanResource p : project.getHumanResourceManager().getResources()) { | |||
| if (p.getDaysOff() != null) | |||
| * | ||
| * @return true if the interval was there and has been removed, false if there was nothing to do | ||
| */ | ||
| public boolean removeDaysOff(GanttDaysOff gdo) { |
There was a problem hiding this comment.
This method is not yet used, right? Do we have any plans of using it?
Anyway, since we delegate removal to the list, it makes sense to define equals and hashCode methods in GanttDaysOff
What is wrong
HumanResource.addDaysOff()has no counterpart. The only way to take an absenceaway is to reach into the
DefaultListModelthatgetDaysOff()hands out andmutate it directly, and nothing observes that model. Adding an absence and
removing one are therefore not the same kind of operation: adding resets the
cached
LoadDistributionand firesfireResourceChanged(), removing doesneither.
Steps to reproduce
Expected: the block disappears, and anything listening to the resource model is
told that something changed.
Actual: the block stays, and no
resourceChangedevent is fired at all.Cause
GanttDialogPerson.applyChanges()clears the list before writing the editedintervals back:
With at least one interval left, the last
addDaysOff()repairs both byaccident. Delete the last absence and the loop body never runs, so nothing
calls
resetLoads()and nothing fires.resetLoads()drops the memoisedmyLoadDistribution, which is the only cache of the computed absences — that iswhy the chart keeps drawing the old picture.
That dialog line is the symptom rather than the defect. A fix that only makes
that one line fire leaves the getter handing out an unobserved mutable model,
and the next caller that removes an absence — a batch edit, an importer, a
script — reintroduces the same gap.
What this changes
1.
getDaysOff()hands out an unmodifiableListview, and every change goesthrough a method. The field is a plain
ArrayList<GanttDaysOff>and the getterreturns
Collections.unmodifiableListover it, typedList<GanttDaysOff>—DefaultListModelis a Swing class and models a UI control, not a resource. Aview rather than a copy, so a caller holding on to it keeps seeing the resource.
addDaysOff(),removeDaysOff()andclearDaysOff()each callonDaysOffChanged(), which doesresetLoads()andfireResourceChanged().The two halves depend on each other: a plain
Listcannot be observed, so thenotification has to live in the methods, and that is only sound because no
caller can reach past them any more.
removeDaysOff()fires only when something was really removed, andclearDaysOff()returns early on an already empty list — whatDefaultListModel.removeElement()andclear()did by themselves.The copying constructor fills the list directly with
addAll()rather thangoing through
addDaysOff(), so copying stays silent without depending onareEventsEnabledfor the days off at all.2. The two missing counterparts, in the shape
HumanResourceManageralreadyuses for its own list (add / remove / clear):
removeDaysOffreturns whether anything was removed, which is also the answer to"did the listeners hear about it" — nothing fires when the interval was not
there. It is
clearDaysOffrather than a bareclear()because a resource holdsassignments and custom properties too.
One caveat, documented on the method and pinned by a test rather than changed
here:
removeDaysOffmatches byObject.equals, the way the list matches.GanttDaysOffonly overloadsequals(GanttDaysOff)and does not overrideequals(Object), so an interval built afresh from the same two dates is not theone the resource holds. Callers should pass an instance obtained from the
resource.
GanttDialogPerson.applyChanges()now callsclearDaysOff()instead ofgetDaysOff().clear(), and no production code can write to the handed-out listany more. The readers of
getDaysOff()move fromgetSize()/getElementAt(i)to
size()/get(i);LoadDistributionandProjectFileExporterwere readingthrough a raw
DefaultListModeland lose their casts.The number of notifications does not change
Clearing and then writing N intervals back still costs
(M > 0 ? 1 : 0) + NresourceChangedevents, where M is the number of intervals before OK: one eventfor the whole removal, none at all when there was nothing to remove. With a plain
Listthat is not free —List.clear()has no notion of firing — soclearDaysOff()returns early on an empty list andremoveDaysOff()returnsfalsewithout firing when nothing matched. The single event this PR adds isexactly the one that was missing in the "delete the last absence" case.
Tests
HumanResourceDaysOffTestremoving the last day off notifies the listeners just as adding it doesHumanResourceRemoveDaysOffTestremoving a single day off takes it off the resource and notifies onceremoving a day off the resource does not have changes nothing and notifies nobodyclearing the days off empties the list and notifies once, whatever the countclearing an empty list of days off notifies nobodythe dialog's clear-all-and-rewrite still costs the same notifications—walks every combination of M and N in
0..3, pins the absolute event countsfor
clearDaysOff()so that an implementation firing once per interval wouldbe caught, and pins from the other side that reaching into the handed-out list
is rejected, removes nothing and notifies nobody.
HumanResourceDaysOffViewTestthe list handed out by getDaysOff cannot be modified—clear,add,removeAtandseton the returned list all throwUnsupportedOperationException, and the resource is unchanged afterwards.the list handed out by getDaysOff is a view and not a copy— a day off addedafter the getter call shows up in the list fetched before it.
Without the notification the first test fails with
expected: <1> but was: <0>,and without the unmodifiable view the first test of
HumanResourceDaysOffViewTestfails with
Expected java.lang.UnsupportedOperationException to be thrown, but nothing was thrown../gradlew test --continueis green on the branch: 370 testson master, 378 on the branch.
Two existing tests changed with the view: the regression test for the last
absence now removes through
clearDaysOff()instead ofgetDaysOff().clear(),and the M/N test turned its "old route" half into the assertion that the old
route is now rejected.